--[[
	Copyright (c) 2012 Eyal Shilony

	Permission is hereby granted, free of charge, to any person obtaining
	a copy of this software and associated documentation files (the
	"Software"), to deal in the Software without restriction, including
	without limitation the rights to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to
	permit persons to whom the Software is furnished to do so, subject to
	the following conditions:

	The above copyright notice and this permission notice shall be
	included in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN 
	
	NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
	LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
	OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
	WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

	-- A lightweight library that provides a common infrastructure for addons.
]]

assert(LibStub, "LibCore-1.0 requires LibStub")

__LibCoreAddonName, __LibCoreAddonTable = ...
local lib, minor = LibStub:NewLibrary("LibCore-1.0", 0)
if not lib then return end
minor = minor or 0

local type = type
local error = error
local pcall = pcall
local format = format
local select = select
local geterrorhandler = geterrorhandler
local debugstack = debugstack
local setmetatable = setmetatable
local unpack = unpack
local tinsert = tinsert
local tremove = tremove

local object = { }
local modules = { }
local events = { }
local methods = { }
local timers = { }
local heap = { }
local loadqueue = { }

local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_LOGOUT")

--[[ LOCALIZATION ]]

local L = {
	["BAD_ARGUMENT"] = "bad argument #%d to '%s' (%s expected, got %s)",
	["MALFORMED_PATTERN"] = "'malformed pattern in '%s'.",
	["LIBCORE_ALREADY_REGISTERED"] = "An instance of libcore was already registered for addon '%s'.",
	["MODULE_ALREADY_DEFINED"] = "the module '%s' is already defined.",
	["MODULE_NOT_DEFINED"] = "the module '%s' is not defined.",
	["EVENT_ALREADY_REGISTERED"] = "the event '%s' is already registered.",
	["EVENT_HANDLER_NOT_FOUND"] = "event handler was not found for '%s'.",
	["EVENT_CANNOT_BE_TRIGGERED"] = "the event '%s' is a built-in event and cannot be triggered manually.",
	["EVENT_CANNOT_BE_REGISTERED"] = "the event '%s' cannot be registered, use the built-in event '%s'.",
	["SCHEDULE_DELAY_LESS_THAN_ZERO"] = "schedule delay time is less than zero.",
}

--[[ LIBRARY HELPER METHODS ]]

local function assert(level, condition, message, ...)
	if condition then
		error(message:format(...), level)
	end
end

local function argcheck(value, num, ...)
	if type(num) ~= "number" then
		error(L["BAD_ARGUMENT"]:format(2, "argcheck", "number", type(num)), 1)
	end

	for i = 1, select("#", ...) do
		if type(value) == select(i, ...) then return end
	end

	local types = strjoin(", ", ...)
	local name = string.match(debugstack(2, 2, 0), ": in function [`<](.-)['>]")
	error(L["BAD_ARGUMENT"]:format(num, name, types, type(value)), 3)
end

local function safecall(func, ...)
	local success, err = pcall(func, ...)
	if not success then
		geterrorhandler()(err)
	end
end

--[[ EVENTS MANAGEMENT ]]

local function fire(events, event, ...)
	if events and type(events) == "table" then
		for module, func in pairs(events) do
			if type(func) == "string" then
				assert(3, not module[func], L["EVENT_HANDLER_NOT_FOUND"], event)
				if type(module[func]) == "function" then
					safecall(module[func], module, event, ...)
				end
			else
				safecall(func, module, event, ...)
			end
		end
	end
end

--[[ RegisterEvent(event[, func]) - Registers an event.

	Arguments.
	event - (string) The event to register.
	func - (func) The event handler method to handle the event.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	core:RegisterEvent("PLAYER_LOGIN", function(self, event)
	end)
	
	Registers a custom event.
	
	core:RegisterEvent("OnFoo", function(self, event)
	end)
	
]]
function methods:RegisterEvent(event, func)
	argcheck(event, 2, "string")
	argcheck(func, 3, "function", "nil")
	local isCustom = false
	if event:find("^On") then
		isCustom = true
		event = self.__owner .. "_" .. event
	end
	assert(3, events[event] and events[event][self], L["EVENT_ALREADY_REGISTERED"], event)
	assert(3, event == "ADDON_LOADED", L["EVENT_CANNOT_BE_REGISTERED"], event, "OnLoad")
	assert(3, event == "PLAYER_LOGIN", L["EVENT_CANNOT_BE_REGISTERED"], event, "OnEnabled")
	assert(3, event == "PLAYER_LOGOUT", L["EVENT_CANNOT_BE_REGISTERED"], event, "OnDisabled")
	func = func or event
	if not events[event] then
		events[event] = { }
		if not isCustom then
			frame:RegisterEvent(event)
		end
	end
	events[event][self] = func
end

--[[ RegisterEvents(...[, func]) - Registers events.

	Arguments.
	args - (table) The list of events to register.
	func - (func) The event handler method to handle the events. (this should always be the last argument you provide.)
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	core:RegisterEvents("PLAYER_LOGIN", "PLAYER_ENTERING_WORLD", function(self, event)
	end)
	
	Registers custom events.
	
	core:RegisterEvents("OnFoo1", "OnFoo2", function(self, event)
	end)
	
]]
function methods:RegisterEvents(...)
	local event
	local count = select("#", ...)
	local lastArg = select(count, ...)
	if type(lastArg) ~= "function" then
		lastArg = nil
	end
	for i = 1, count do
		if not lastArg or (lastArg and i <= count - 1) then
			event = select(i, ...)
			methods.RegisterEvent(self, event, lastArg)
		end
	end
end

--[[ UnregisterEvent(event) - Unregisters an event.

	Arguments.
	event - (string) The event to unregister.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	core:UnregisterEvent("PLAYER_LOGIN")
	
	Unregisters a custom event.
	
	core:UnregisterEvent("OnFoo")
	
]]
function methods:UnregisterEvent(event)
	argcheck(event, 2, "string")
	if event:find("^On") then
		event = self.__owner .. "_" .. event
	end
	local e = events[event]
	if e then
		e[self] = nil
		if not next(e) then
			events[event] = nil
			if not event:find("^On") then
				frame:UnregisterEvent(event)
			end
		end
	end
end

--[[ UnregisterEvents(...) - Unregisters events.

	Arguments.
	args - (table) The list of events to unregister.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	core:UnregisterEvents("PLAYER_LOGIN", "PLAYER_ENTERING_WORLD")
	
	Unregisters custom events.
	
	core:UnregisterEvents("OnFoo1", "OnFoo2")
	
]]
function methods:UnregisterEvents(...)
	local event
	for i = 1, select("#", ...) do
		event = select(i, ...)
		methods.UnregisterEvent(self, event)
	end
end

--[[ IsEventRegistered(event) - Determines whether an event is registered.

	Arguments.
	event - (string) The event to check whether is registered.
	
	Returns.
	registered - (boolean) true if registered; otherwise, false.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	core:IsEventRegistered("PLAYER_LOGIN")
	
	Determines whether a custom event is registered.
	
	core:IsEventRegistered("OnFoo")
	
]]
function methods:IsEventRegistered(event)
	argcheck(event, 2, "string")
	if event:find("^On") then
		event = self.__owner .. "_" .. event
	end
	local e = events[event]
	if e and e[self] then
		return true
	else
		return false
	end
end

--[[ TriggerEvent(event, ...) - Triggers a custom event.

	Arguments.
	event - (string) The event to trigger.
	args - (table) An arbitrarily list of arguments.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	Triggers a custom event.
	
	core:TriggerEvent("Foo")
	
	Note, when you trigger a custom event, you shouldn't specify the prefix "On" as it's added by default.
	
]]
local function triggerEvent(self, event, isBuiltIn, ...)
	event = "On" .. event
	assert(3, not isBuiltIn and (event == "OnLoad" or event == "OnEnabled" or event == "OnDisabled"), L["EVENT_CANNOT_BE_TRIGGERED"], event)
	fire(events[self.__owner .. "_" .. event], event, ...)
end

function methods:TriggerEvent(event, ...)
	argcheck(event, 2, "string")
	triggerEvent(self, event, false, ...)
end

--[[ TASKS SCHEDULING ]]

local function heapSwap(i1, i2)
	heap[i1], heap[i2] = heap[i2], heap[i1]
end

local function heapBubbleUp(index)
	while index > 1 do
		local parentIndex = math.floor(index / 2)
		if heap[index].__interval < heap[parentIndex].__interval then
			heapSwap(index, parentIndex)
			index = parentIndex
		else
			break
		end
	end
end

local function heapBubbleDown(index)
	while 2 * index <= heap.lastIndex do
		local leftIndex = 2 * index
		local rightIndex = leftIndex + 1
		local current = heap[index]
		local leftChild = heap[leftIndex]
		local rightChild = heap[rightIndex]
		if not rightChild then
			if leftChild.__interval < current.__interval then
				heapSwap(index, leftIndex)
				index = leftIndex
			else
				break
			end
		else
			if leftChild.__interval < current.__interval or
			   rightChild.__interval < current.__interval then
				if leftChild.__interval < rightChild.__interval then
					heapSwap(index, leftIndex)
					index = leftIndex
				else
					heapSwap(index, rightIndex)
					index = rightIndex
				end
			else
				break
			end
		end
	end
end

local function cancelTimer(self)
	self.__cancelled = true
end

local function timer(schedule, delay, func, ...)
	argcheck(schedule, 1, "table")
	
	timers[schedule] = func
	schedule.__interval = GetTime() + delay
	
	if select('#', ...) ~= 0 then
		schedule.__args = { ... }
	end

	if heap.lastIndex then
		heap.lastIndex = heap.lastIndex + 1
	else
		heap.lastIndex = 1
	end
	
	heap[heap.lastIndex] = schedule
	heapBubbleUp(heap.lastIndex)
	
	if not frame:IsShown() then
		frame:Show()
	end
	
	return schedule
end

--[[ CreateTask(delay, func, ...) - Creates a one time task.

	Arguments.
	delay - (number) The time interval to fire the task.
	func - (function) The function to be fired.
	args - (table) An arbitrarily list of arguments.
	
	Returns.
	task - (table) A task.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	local task = core:CreateTask(0.1, function(self)
	end)
	
	To cancel a task use the cancel method like so.
	
	task:Cancel()
	
]]
function methods:CreateTask(delay, func, ...)
	argcheck(delay, 2, "number")
	argcheck(func, 3, "function")
	assert(3, delay < 0, L["SCHEDULE_DELAY_LESS_THAN_ZERO"])
	local schedule = { }
	schedule.__module = self
	schedule.Cancel = cancelTimer
	return timer(schedule, delay, func, ...)
end

--[[ ScheduleTask(delay, func, ...) - Schedules a repetitive task.

	Arguments.
	delay - (number) The time interval to fire the task.
	func - (function) The function to be fired.
	args - (table) An arbitrarily list of arguments.
	
	Returns.
	task - (table) A scheduled task.
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
	local task = core:ScheduleTask(0.1, function(self)
	end)
	
	To cancel a task use the cancel method like so.
	
	task:Cancel()
	
]]
function methods:ScheduleTask(delay, func, ...)
	local schedule = methods.CreateTask(self, delay, func, ...)
	schedule.__delay = delay
	return schedule
end

--[[ MODULES ]]

--[[ NewModule(name) - Creates new module.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	module - (table) A module.
	
]]
function object:NewModule(name)
	argcheck(name, 2, "string")
	local modules = modules[self]
	assert(2, modules[name], L["MODULE_ALREADY_DEFINED"], name)
	local module = { }
	module.__name = name
	module.__owner = self
	setmetatable(module, { __index = methods })
	modules[name] = module
	return module 
end

--[[
 GetModule(name) - Gets the given module.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	module - (table) A module.
	
]]
function object:GetModule(name)
	argcheck(name, 2, "string")
	local modules = modules[self]
	local module = modules[name]
	assert(2, not module, L["MODULE_NOT_DEFINED"], name)
	return module
end

--[[ IsModuleRegistered(event) - Determines whether the module is registered.

	Arguments.
	name - (string) The name of the module.
	
	Returns.
	registered - (boolean) true if registered; otherwise, false.
	
]]
function object:IsModuleRegistered(name)
	argcheck(name, 2, "string")
	local modules = modules[self]
	local module = modules[name]
	if module then
		return true
	else
		return false
	end
end

--[[ IterateModules() - Traverses through all modules. ]]
function object:IterateModules()
	local modules = modules[self]
	return pairs(modules)
end

--[[ LIBRARY EVENTS ]]

function frame:ADDON_LOADED(event, name)
	local modules, module = modules[name]
	if modules then
		module = modules["Core"]
		triggerEvent(module, "Load", true)
		tinsert(loadqueue, module)
	end
end

function frame:PLAYER_LOGIN(event)
	local module = tremove(loadqueue, 1)
	while module do
		triggerEvent(module, "Enabled", true)
		module = tremove(loadqueue, 1)
	end
end

function frame:PLAYER_LOGOUT(event)
	local module
	for name, addon in pairs(modules) do
		module = addon["Core"]
		triggerEvent(module, "Disabled", true)
	end
end

local function onEvent(self, event, ...)
	if frame[event] then
		frame[event](self, event, ...)
		return
	end
	fire(events[event], event, ...)
end

local function onUpdate(self, elapsed)
	local schedule = heap[1]
	while schedule and schedule.__interval < GetTime() do
		if schedule.__cancelled then
			heapSwap(1, heap.lastIndex)
			heap[heap.lastIndex] = nil
			heap.lastIndex = heap.lastIndex - 1
			timers[schedule] = nil
		else
			local func = timers[schedule]
			if schedule.__args then
				safecall(func, schedule.__module, unpack(schedule.__args))
			else
				safecall(func, schedule.__module)
			end
			if schedule.__delay then
				schedule.__interval = GetTime() + schedule.__delay
			else
				schedule:Cancel()
			end
		end
		heapBubbleDown(1)
		schedule = heap[1]
	end
	if not schedule then self:Hide() end
end

frame:SetScript("OnEvent", onEvent)
frame:SetScript("OnUpdate", onUpdate)

--[[ LIBRARY INITIALIZATION ]]

--[[ New() - Creates a new instance of the library.
	
	Returns.
	name - (string) The addon's name.
	addon - (table) The addon's global table. (This is the same table defined by Blizzard for addons, this also provides the access point to the LibCore APIs.)
	
	Usage.
	local addonName, addon, core = LibStub("LibCore-1.x"):New()
	
]]
function lib:New()
	local addonName, addon = __LibCoreAddonName, __LibCoreAddonTable
	assert(1, modules[addonName], L["LIBCORE_ALREADY_REGISTERED"], addonName)
	modules[addonName] = { }
	setmetatable(addon, { __index = function(self, key)
		local method = object[key]
		if method and type(method) == "function" then
			local func = function(self, ...)
				return method(addonName, ...)
			end
			return func
		else
			return
		end
	end })
	local core = addon:NewModule("Core")
	return addonName, addon, core
end